课程总结:AI 组件库从 0 到 1 复盘
20章课程接近尾声。这节复盘 AI Chat UI 组件库的完整构建过程,梳理每个阶段的决策、踩坑和收获,并补充真实生产环境的踩坑经验。
项目全貌回顾
从第10章到第19章,我们用 10 章的篇幅构建了一个完整的 AI 聊天组件库。回顾整个架构:
@toimc/core 类型定义、useChat、ChatAdapter 接口、GenUI schema、token 估算
@toimc/vue Conversation/Message/PromptInput 组件族、语音 composables、GenUIRenderer、图片压缩
@toimc/markdown Markdown 渲染、代码高亮、公式渲染
@toimc/agents 模型适配器(多协议 + /mastra 子路径)、工具定义、usage 回传
@toimc/server Hono 网关、SSE 流式输出、identity 鉴权与配额、用量记账
(私有包)dev-server / playground / docs
text
第18章的前沿体验(生成式 UI、语音、端侧 AI)主要落在 @toimc/vue 与 playground;多用户鉴权与用量治理作为工程能力落在 @toimc/server 的 identity 模块(实现核查见第19章 resources 的核查报告);第19章课程主题最终定为离线与内网环境下的 AI 编码工具实战——包边界没有因为这些新方向而重划,这是前期分层决策的直接回报。
分阶段复盘
第一阶段:核心层(第10章)
做了什么:定义 ChatAdapter 接口、实现 useChat composable、构建五个 Vue 组件、Markdown 渲染。
关键决策:
- core 包不依赖 Vue,任何框架都能用(第10-01)
- ChatAdapter 用 AsyncGenerator 抽象流式响应,适配所有 AI 服务商(第10-02)
- 组件用 CSS Variables 做主题,不绑定 CSS 框架(第10-03)
- Markdown 渲染分层:MVP 用正则,Shiki/KaTeX 按需升级(第10-04)
踩过的坑:
- StreamText 组件的 watch 要配合 CSS animation 才能有光标效果
- Markdown 渲染必须先转义 HTML 再解析语法,顺序反了会出 XSS
- useChat 的 abort 逻辑应该更早设计,第一版遗漏了 AbortController
第二阶段:样式系统(第11章)
做了什么:三层 Design Token、暗色模式、主题预设、容器查询、样式隔离。
关键决策:
- Primitive → Semantic → Component 三层令牌,暗色模式只改 Semantic 层(第11-01)
- 容器查询替代 @media,组件适配任意容器宽度(第11-02)
- CSS Layers 分层而非 Shadow DOM,平衡隔离性和易用性(第11-03)
- 命名规范应该一开始就定好,后改成本很高(第11-01遗留问题)
第三阶段:高级组件(第12章)
做了什么:多模态消息、工具调用面板、多会话管理、国际化、API 审查。
关键决策:
- Message 类型用可选 attachments 渐进增强,不影响现有文本渲染(第12-01)
- ToolCall 可视化让用户看到 AI 的决策过程(第12-02)
- i18n 自研轻量方案不到 50 行,不引入 vue-i18n(第12-04)
- i18n 应该和组件同步做,后补翻译 key 容易遗漏
第四阶段:质量与发布(第13-14章)
做了什么:Vitest 单元测试、Vue 组件测试、覆盖率门禁、npm 发布、Changeset 自动化。
关键决策:
- core 层覆盖率目标 90%+,vue 层 70%+,按纯度区分(第13-03)
- jsdom 环境需要 polyfill matchMedia 和 ResizeObserver(第13-02)
- Changeset 管理 Monorepo 版本联动,发布顺序按依赖方向 core→vue→markdown(第14-03)
- exports 子路径设计支持按需加载(第14-01)
第五阶段:服务端与Agent(第15-17章)
做了什么:agents/tools/server 三个包、Hono 流式服务、ReAct Agent、MCP 宿主化、插件系统、记忆系统。
关键决策:
- agents 和 tools 包零外部依赖,纯逻辑层可移植(第15-01)
- Agent 用 ReAct 模式:思考→行动→观察循环(第15-02)
- MCP 宿主化:3-3 是给 Claude Code 做 server(使用者),17-01 是让 ai-chat-ui 连接外部 server(宿主),
listTools()动态发现 + 命名空间防冲突(第17-01) - 插件系统配置驱动,运行时可开关(第17-02)
- 记忆的 thread/resource 双层隔离在网关 API 设计时就定了,多用户场景没有返工(第17-03)
- 应该更早引入 streaming 的错误恢复,不该假设流一定完整
第六阶段:前沿体验与内网实战(第18-19章)
做了什么:生成式 UI(工具结果直接渲染组件而非 JSON 面板)、语音与多模态交互、浏览器端 AI(第18章,全部真实落地);多用户鉴权、会话隔离、配额与用量记账作为工程能力补齐进 server 包;第19章转向离线与内网环境下的 AI 编码工具实战(Claude Code/Codex 接入、私有化选型、迭代更新)。
关键决策:
- 工具结果按"可渲染数据"设计契约,ToolCall 系列从 JSON 面板升级为生成式 UI 的起点(第18-01)
- 语音、端侧 AI 按 12 章的渐进增强思路接入:可选能力,不影响已有渲染链路(第18章)
- 按角色选模型(简单问题用小模型、复杂问题才用大模型)是成本控制的第一杠杆(第16章)
- 多用户鉴权和用量计量落地为 identity 模块:thread 归属校验 + resource 服务端覆写 + usage 贯通,AUTH_MODE 门控保持向后兼容(工程落地记录见第19章 resources 核查报告)
代码规模统计
基于第10-19章的完整实现(统计为约数,随章节重构略有浮动):
包名 文件数 代码行数 测试行数 覆盖率
@toimc/core 14 1094 1749 门禁 90%+
@toimc/vue 104 10995 10561 门禁 70%+
@toimc/markdown 16 1575 1287 门禁 75%+
@toimc/agents 8 958 1565 —
@toimc/server 15 1110 1884 —
─────────────────────────────────────────────────────
总计 157 15732 17046 全量 1705 用例
text
(2026-10-05 按 packages/*/src 与 tests 实测;覆盖率列为各包 CI 门禁阈值。原统计表中的 tools 包已在 16-01 重构中并入 agents/server;第18-19章新增的 GenUI、语音、多模态、identity、端侧搜索使 vue 包规模显著上浮。)
开发效率回顾
各阶段的 AI 辅助程度:
| 阶段 | AI 做什么 | 人做什么 | 采纳率 |
|---|---|---|---|
| 接口设计 | 生成初始接口代码 | 审查设计决策,调整命名和类型 | 60% |
| 组件开发 | 生成组件模板和样式 | 调整交互细节,验证无障碍性 | 65% |
| 测试编写 | 生成测试用例 | 审查覆盖率,补充边界用例 | 75% |
| 工具/插件/MCP | 生成适配器和执行器代码 | 审查安全边界、超时与降级策略 | 60% |
| 调试排查 | 分析错误信息,定位问题 | 确认修复方案,验证效果 | 70% |
| 文档编写 | 生成 API 文档模板 | 校准描述准确性 | 80% |
AI 生成代码的采纳率变化:
- 基础组件(10-12章):60-70%
- 服务端和Agent(15-17章):50-65%
- 前沿体验与内网实战(18-19章):45-55%
采纳率下降的原因:越靠近产品和真实环境,定制化程度越高——生成式 UI 的组件契约、企业内网的选型约束这些强依赖具体产品形态的逻辑,AI 生成的代码往往"能用但不贴业务",需要大量人工调整。
生产环境事故案例
两个真实的生产问题和处理经验:
案例1:流式渲染内存泄漏
现象:用户反馈长时间聊天后浏览器变卡,内存占用持续上升。
定位过程:
- Sentry 报告
Message组件频繁更新 - 性能监控显示
scrollFps从 60fps 降到 20fps - Chrome DevTools Memory Profiler 发现大量 detached DOM 节点
根本原因:
- StreamText 组件每次收到 chunk 都创建新的 Text Node,旧节点未清理
- useChat 的
messagesref 没有设置maxHistory上限
修复方案:
// 在 StreamText 中添加清理逻辑
onBeforeUnmount(() => {
if (textNode.value && textNode.value.parentNode) {
textNode.value.parentNode.removeChild(textNode.value)
}
})
// 在 useChat 中添加上限
const maxHistory = 100 // 最多保留100条消息
watch(messages, (newMessages) => {
if (newMessages.length > maxHistory) {
messages.value = newMessages.slice(-maxHistory)
}
})
typescript
预防措施:
- 增加
memoryUsage指标监控,超过 200MB 告警 - 每个会话建议超过 50 条消息时提示用户清空历史
和第17-03的呼应:记忆系统的 lastN 滑窗、maxHistory 上限本质是同一个思想——前端展示层和后端记忆层都要"存得多、喂得少",否则内存和 token 至少一个先爆。
案例2:工具调用超时导致请求卡死
现象:Agent 调用搜索工具后一直没有响应,前端显示"思考中"一直转圈。
定位过程:
- Trace 追踪显示
tool_callstep 有startTime但没有endTime - 检查搜索工具日志,发现第三方 API 响应时间超过 30 秒
- Controller 没有设置超时保护
根本原因:
- 第15章的 Orchestrator 缺少超时配置
- 工具执行器没有
AbortSignal超时机制
修复方案:
// 在 Orchestrator 中添加超时
const timeout = 10000 // 10秒超时
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const result = await this.executor.execute(toolName, args, { signal: controller.signal })
clearTimeout(timeoutId)
return result
} catch (error) {
if (error.name === 'AbortError') {
return { error: 'Tool execution timeout' }
}
throw error
}
typescript
预防措施:
- 所有工具调用设置 10 秒超时
- 超时的工具自动降级或重试(最多3次)
- 监控
toolCallSuccessRate,低于 80% 告警
和第17-01的呼应:MCP 宿主化之后这个问题更突出——外部 server 的响应时间完全不可控,SafeToolExecutor 的超时和响应大小限制(17-01)就是为它准备的。
成本账本
基于第16章的成本实战,一个真实的成本分析:
月度成本结构(假设 10万 日活):
- LLM 调用:800/月(ClaudeSonnet5,0.003/1K tokens)
- 第三方工具:$200/月(搜索 API、天气 API)
- 基础设施:$150/月(Vercel 托管、数据库)
- 总计:$1150/月
成本优化策略:
- 按角色选模型:简单问题用 Haiku(便宜 70%),复杂问题才用 Opus(贵但准确)——这是第一杠杆
- 工具调用结果缓存 5 分钟(减少重复调用)
- 用户限流:每分钟最多 30 次请求
效果:优化后成本降到 $650/月,节省 43%。
对成本的意义:1150还是650,直接决定你能不能定价。按量计费的产品如果单位成本算不清,卖得越多亏得越多——这就是用量记账(usage 贯通)要在架构层做的原因。(日常使用侧的省钱技巧与轻量工具 pi,见 20-03 ~ 20-05。)
安全素养
第17章讲了 MCP 鉴权和错误恢复,多用户鉴权与配额已落地 server 包的 identity 模块(thread 归属校验、API Key 哈希存储、402 配额响应,工程记录见第19章 resources 核查报告),从安全角度看还有三块要补:
常见安全漏洞
1. XSS 攻击
- 风险点:Markdown 渲染、用户输入的文本内容
- 防护:DOMPurify 清理、CSP 策略、转义 HTML
2. 提示词注入
- 风险点:用户输入的内容被直接拼接到 system prompt
- 防护:输入过滤、检测恶意模式、限制 prompt 长度
3. 敏感信息泄露
- 风险点:日志中可能包含用户隐私、API Key
- 防护:PII 识别和脱敏、密钥轮换、访问控制
安全 Checklist
发布前必须检查:
- 所有用户输入都经过转义和清理
- Markdown 渲染使用 DOMPurify
- API Key 不出现在代码仓库(用环境变量)
- 敏感操作需要鉴权(工具调用、会话删除)
- 错误信息不暴露内部实现细节
- 依赖包没有已知漏洞(
npm audit)
核心收获
- 接口设计比实现重要:ChatAdapter 接口花了两版才定下来,但定下来后 core 和 server 的对接几乎零摩擦
- 分层设计降低复杂度:core/vue/markdown 三个包各司其职,18-19章新增能力没有重划包边界
- 渐进增强优于一步到位:Markdown 从正则到 Shiki、ToolCall 从简单到面板、多模态从可选 attachments 到语音,每一步都能用
- 测试是最好的文档:core 层的测试用例就是 ChatAdapter 的使用说明
- 配置优于硬编码:主题用 CSS Variables、插件用 YAML、MCP server 用声明式清单,用户定制不需要改源码
- 工具结果定制渲染比 JSON 面板更接近产品:ToolCall 系列展示 JSON 只是调试视角,把工具结果按数据类型渲染成卡片、图表、表单(第18章生成式 UI),用户感受到的才是产品而不是调试器
- 按角色选模型是成本控制的第一杠杆:同样的功能,模型路由策略的差别就是月账单差 40% 以上;缓存和限流是第二梯队
- 商业化要在设计接口时就想好:多用户鉴权、用量计量这些字段,第一天在 API 层预留成本最低;等要收费了再补,改的是全链路
如果重新来过
几个会调整的地方:
- 第10章:useChat 的 abort 逻辑应该更早设计,第一版遗漏了 AbortController;ChatAdapter 接口应该一开始就考虑 streaming 错误恢复
- 第11章:Design Token 命名规范应该一开始就定好,后改成本很高
- 第12章:i18n 应该和组件同步做,后补翻译 key 容易遗漏;工具调用结果应该在定义 StreamChunk 时就约定"可渲染"的数据结构,而不是先给 JSON 再改造
- 第15章:应该更早引入 streaming 的错误恢复,不该假设流一定完整;工具调用超时应该在架构设计阶段就考虑
- 第17章:MCP 宿主的连接清理(stdio 子进程)应该在第一次挂载时就写,而不是部署后发现有僵尸进程再补
- 第18章:生成式 UI 的组件契约应该与工具定义同步设计——工具返回什么结构,决定了前端能渲染什么,两边各做各的会返工两次
- 第19章:内网与离线不是"以后再想"的约束——端点协议、模型许可、更新流水线这三件事,在工具选型第一天就要想清楚,等环境 locked down 之后再改,改的是全链路
最大的教训:体验和工程化约束不是"发布后再想"的事。生成式 UI 的契约要在接口设计第一天留好位置,离线/内网的协议与许可要在选型第一天想清楚——这和"abort 逻辑要早设计"是同一个道理。
小结
- 10 章内容覆盖了组件库从设计、发布到服务端、Agent、前沿体验与内网实战的完整链路
- AI 辅助开发提升了编码速度,但架构决策和接口设计仍需人工把关;越靠近产品,采纳率越低
- 分层、渐进增强、配置驱动是贯穿全文的三个设计原则
- 两个生产事故的共同根因都是"没设上限":内存没有上限、时间没有上限——边界条件要从第一天就设
- 成本和安全是产品化的地基:按角色选模型控制成本下限,鉴权计量决定商业上限
下一节展望 AI 时代的前端趋势、技能地图和持续学习路径。
↑